【全文書き起こしてみた】Developers.IO 2020 CONNECT「VIPERで作ろう! 実践iOSアプリ開発 〜録画したライブコーディングを添えて〜」
はじめに
まいど。CX事業本部の中安です。
今回は Developers.IO 2020 CONNECT で動画発表させていただきました「VIPERで作ろう! 実践iOSアプリ開発 〜録画したライブコーディングを添えて〜」の内容の全文書き起こしになります。
動画内で作ったサンプルアプリのソースコードも置いてあります。
以降は、動画の全文になります。聞き取れなかった、もしくは分かりづらかったという箇所の確認としてご利用ください。
冒頭
みなさん、こんにちわ。こんばんわ。
クラスメソッド株式会社 CX事業本部の中安と申します。 普段はiOSアプリをメインにモバイルアプリの開発エンジニアをやっています。よろしくお願いします。
今回は、iOSアプリ開発のための設計アプローチである「VIPER」についてのお話をさせていただきます。
アジェンダ
まずは今回のアジェンダです。
最初に、VIPERについての基本的な概要をお話させていただこうかと思います。
次に、VIPERを使って実際にソースコードを書きながら、ひとつの簡単なアプリを作っていこうと思います。こちらは長丁場になりますが、楽しんでいただければなぁというふうに思います。
そして、VIPERの各要素について、実際に使ってみての感触なども交えて、更に深堀りをする内容をお話できればというふうに思っております。
この動画の対象
この動画の対象となる方ですが「iOSアプリの開発を現在実際にされている方」「これからやってみようと思っている方」。 そして「VIPERって聞いたことない」または「VIPERという言葉自体は知ってるけど、今まで触れてこなかったよ」という方が対象になります。
VIPERの概要
それでは本題に入っていきましょう。
まずは、VIPERという言葉をご存知でしょうか。 VIPERはアプリケーションの設計アーキテクチャー、システムアーキテクチャのひとつに挙げられるものです。
今回でいう「システムアーキテクチャー」というのは「アプリケーションを開発する上での、クラスなどの要素、つまり登場人物同士の関係性を整理し、 伝達順序を整理し、規則性を持たせて、開発者同士の共通認識として持っておく構成」という定義でお話をさせていただきたいと思います。
AppleMVCとその弊害
iOSアプリの開発においては、そもそもがAppleが提唱している、いわゆる「AppleMVC」というシステムアーキテクチャがベースとして存在しています。
AppleMVCでは、画面のメインであるUIViewが1つ配置され、その中に多くのサブビューが置かれます。それらを総じてView「V」と呼び、 このViewをビューコントローラ、すなわち「C」が操作していきます。 UIViewが「V」、UIViewControllerが「C」を担っているAppleMVCですが、「M」つまりモデルにあたる基底クラスなるものは明確には存在していません。
このことによって引き起こされてきた問題とは何でしょうか。
それは開発者によってコントローラとモデルの関係性の認識が異なっていて、実装方法がバラバラになってしまっていたことでした。 バラバラになることで影響範囲のわからないソースコードが繰り返し実装され、 そこにちょっとした機能を足そうとしても工数が多くかかってしまう問題を孕んでいます。
そのようなスパゲッティコードは、得てしてビューコントローラに集約されやすくなります。 なぜなら、iOSアプリの開発においてUIViewControllerの汎用性は高く、それゆえに最終的には「M」にも「C」にも「V」にもなりえてしまうからです。
これらは「Fat ViewController」や「Massive ViewController」と呼ばれるアンチパターンにあたります。 iOSアプリの開発をしていると、時折そういったソースコードを見かけたことがあるかもしれません。 その対策としてAppleMVC以外の様々なシステムアーキテクチャーが存在するのですが、そのうちのひとつが今回お話をするVIPERになります。
VIPERはiOSアプリの開発全体に言及した数少ないアーキテクチャとも言われています。 ただし誤解を招きたくないのは、VIPERを取り入れれば開発上の問題のあらゆることが解決するわけではありません。 月なみですが「銀の弾丸はない」という前提を持っておかなければなりません。
システムアーキテクチャは本来、規模や開発人数、スケジュールに合わせて選定したほうがいいと思いますので そのあたりも考慮した上でVIPERの採用を決めていただければと思います。
VIPERについて
それでは、VIPERについての概要をお話をしていきます。
VIPERは2013年末にミューチュアルモバイルという会社が提唱したシステムアーキテクチャになります。
その以前より提唱されていたクリーンアーキテクチャがもとになっていますが、 汎用性が高いクリーンアーキテクチャは他の様々なシステム開発でも用いられるのに対して、 VIPERはiOSアプリの開発用に再整理されたものになります。
依存性の分離
まずは、VIPERの目的と特徴です。
ひとつめは「依存性の分離」です。 「依存性の分離」とは要素同士が他の要素のことをできるだけ知らない、関心がない状態にすることです。 これはベースになっているクリーンアーキテクチャが目指している理想でもあり、VIPERもそれを踏襲しています。
具体的にどうするのかというと、要素同士はインターフェイス、Swiftではプロトコルのことを指しますが、これらだけで繋がる状態にしておいて、その中で行われることは互いに知らない状態を作っていきます。 さらに、要素の実体(インスタンス)は外部から差し込むことができ、容易に差し替えることができる状態にします。 これを「依存性注入」(Dependency Injection)と呼び、VIPERの特徴のひとつでもあります。
このような設計をすることにより、それぞれの要素単体でテストができる。 つまりテスタブルなソースコードが実現できるようになるというわけです。
単一責任の原則
ふたつめは「単一責任の原則」です。 SRPとも呼ばれる「単一責任の原則」とは「クラスの中身を変更する理由は複数存在しない」という原則になります。
例えば、ビジネスロジックで「データを取得する」「計算する」「結果を送信する」といった処理を(まとめて)ひとつのクラスに行わせると、 互いの処理に干渉する可能性が高くなります。
1つのビジネスロジックは1つのクラスで行わせることにより、責務を切り離して検証することができるようになるというわけです。
VIPERの各要素
次に、そもそもVIPERという名前ですが、これは5つの登場人物の頭文字をとったものです。
その登場人物とは「View」「Interactor」「Presenter」「Entity」「Router」になります。 それぞれにはそれぞれの役割がありますので、もう少し細かく見ていきましょう。
View
1つ目は「V」。Viewです。
Viewは見た目とユーザー操作を受け付ける役割を担当します。 いわゆるUI(ユーザーインターフェイス)のことです。
VIPERにおけるViewはMVPアーキテクチャーと同様に、メインのビューに加え、サブビューも含んだAppleMVC上の「V」と、「C」であるビューコントローラを合わせて指します。
Viewは後で話すPresenterが指示してきた内容を表示するという仕事を担います。
また、ユーザーインタラクション(ユーザーによる操作)や発生するイベントなどをハンドリングしてPresenterにそれを委任させます。 ViewはPresenterからの「次に何を表示するか」の指示が来るまでは待つ状態になります。
Interactor
2つ目は「I」。Interactorです。
Interactorはビジネスロジック。つまり、Viewとは違い見た目とは関係のないロジックの部分を処理する役割を担当します。 主にはデータのやりとり、すなわちクラッド(CRUD)についての仕事をすることが多くなると思います。
あとで登場するEntityの生成とその破棄を受け持つのも、このInteractorの仕事になります。 先ほど登場した「単一責任の原則」は、このInteractorで強く意識することになると思います。
Presenter
3つ目は「P」。Presenterです。
Presenterは他の4つの要素の旗振り役といってもいい役割を受け持っていて、要となる登場人物になります。
Viewのときにも話したとおり、ユーザの操作やイベントを受け付けてInteractorにビジネスロジックを依頼します。 Interactorの結果を受けて、次はViewに対して表示の指示を出します。 また、あとで登場するRouterへの指示もPresenterが行います。
お客さんの注文を聞いて、シェフに伝え、できあがった料理を運んで、お客さんのテーブルに提供する。 そんなレストランのウェイターさんのような絵を浮かべていただければ、イメージが湧きやすいのではないでしょうか。
Entity
4つ目は「E」。Entityです。
Entityはデータそのものを指す登場人物です。 昔ながらの言葉でいうところのVOやDTOといったものに近いかなというふうに思います。
基本的にはオブジェクト変数のみで構成されて、ロジックは持たないのが原則になります。 設計の際には、業務。すなわちドメインにもとづいて抽象的なモデリングがされていることが大切です。
先ほども話したとおり、EntityはInteractorによってその生成と破棄が管理されます。
Router
5つ目は、R。Routerです。
Routerの主な役割は画面遷移です。
おおよそのアプリは1画面だけに留まらず複数の画面を移動することで成り立っています。 Presenterの指示によってRouterはその画面遷移を実行させます。
VIPERにも幾つかの方法に分かれていて一概には言えないのですが、 遷移先の画面を生成する役割もRouterが担うことになります。 そして、その際には、依存性注入を行う仕事も担うことになります。
関係図
ここまでで登場人物のざっくりとした説明になりますが、 VIPERの登場人物は、こちらのような関係図で描かれることが多いです。
ViewはInteractorの仕事については知らない状態になっていますし、 逆に、InteractorはUIについては関心がありません。
PresenterはViewとRouter、Interactorに指示が出せるように中心に位置しています。 Entityは図の上ではこの位置にいますが、Interactorから作られたEntityは、Presenterに運ばれてそれぞれの要素へと渡されていきます。
VIPERでアプリを作ろう(実演)
ここまでで、VIPERの基本的なお話をしてきました。 では、実際にVIPERを使ってアプリを作っていくことにしましょう。
今回作るアプリについて
今回サンプルとして作ろうと思うアプリですけれども、 WebAPIからJSONを取ってきてデータを一覧に出す、その中からユーザがひとつ選ぶと詳細画面に遷移する、 そんなシンプルなアプリを作ってみようと思います。
こちらは、テスト用のREST-APIが色々と用意されている「JsonPlaceholder」というWebサービスになるのですが、
今回は、こちらのposts
というパスのAPIから返ってくるデータを使っていこうと思います。
APIから返るこのデータは投稿された記事を取得できるものなので、モデル名としては、英語でArticle
。日本語では記事
と呼ぶことにします。
下準備
まずは、プロジェクトを新規作成して、ある程度整えたものが、こちらになります。
最初はAppDelegate
。次にSceneDelegate
。
記事の一覧を表示するArticleListViewController
と、そのストーリーボードであるArticleList
。
そして、記事の詳細を表示するArticleDetailViewController
と、ストーリーボードのArticleDetail
だけが用意されている状態です。
ある程度のところまでUIの部分は先に組んであります。
こちらが記事一覧画面のストーリーボードになります。 TableViewが1つだけ置いてあるシンプルなUIです。 TableViewのDelegateと、DataSourceはビューコントローラが担当します。
とりあえずですね、仮組みとして10件「記事のタイトル」という固定の文言が一覧に表示される状態になっています。
import UIKit class ArticleListViewController: UIViewController { @IBOutlet private weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() } } extension ArticleListViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = "記事のタイトル" return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) } }
こちらは記事詳細画面のストーリーボードです。 こちらもTableViewが1つだけ置いてありますが、出す情報としては記事のタイトルと本文だけになります。 その表示のための実装もしています。
import UIKit class ArticleDetailViewController: UIViewController { enum Row: String { case title case body static var rows: [Row] { return [.title, .body] } } @IBOutlet private weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() } } extension ArticleDetailViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return Row.rows.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let row = Row.rows[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: row.rawValue, for: indexPath) if row == .title { cell.textLabel?.text = "タイトル" cell.detailTextLabel?.text = "記事のタイトル" } if row == .body { cell.textLabel?.text = "記事の本文" cell.detailTextLabel?.text = nil } return cell } }
では、実際にアプリでどのように表示されるかを一度試しで出してみようかと思います。
Info.plistをOpen As
> SourceCode
で開き、
UIScreenConfigurations
のUISceneStoryboardFile
の設定を、記事一覧画面のストーリーボード名であるArticleList
に変えて実行してみます。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <!-- 省略 --> <dict> <key>UIApplicationSupportsMultipleScenes</key> <false/> <key>UISceneConfigurations</key> <dict> <key>UIWindowSceneSessionRoleApplication</key> <array> <dict> <key>UISceneConfigurationName</key> <string>Default Configuration</string> <key>UISceneDelegateClassName</key> <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> <key>UISceneStoryboardFile</key> <string>Main</string> </dict> </array> </dict> </dict> <!-- 省略 --> </plist>
このように期待どおり「記事のタイトル」という固定の文言が10件、一覧に表示されました。
では次に、記事詳細画面のストーリーボード名であるArticleDetail
に変えてみます。
実行してみると、こんな感じです。
これで、2つの画面の仮のUIが組みあがったことが確認できました。 こちらは、VIPERの中のViewとして取り扱っていきます。
今、変更した設定は元に戻しておきます。
Entityの作成
それでは、VIPERを使ってアプリを組み立てて行きたいと思います。
Entityの定義
まずは、VIPERの中のEntityを作っていきます。
今回、Entityの名前はArticleEntityという名前にしました。 WebAPIの戻り値であるJSONは、このように返ってきます。 なので、ArticleEntityの構造もこの通りに定義していきます。
まずは記事のID。こちらは整数型なのでIntにします。 そしてユーザID。こちらも整数型なのでIntです。 そしてタイトルは、これは文字列型なのでStringにします。 最後にbody。本文も文字列型なのでStringとしておきます。
このコメントは、消しておきます。
import Foundation struct ArticleEntity { let id: Int let userId: Int let title: String let body: String }
EntityをViewに反映
それでは、記事一覧画面の方に、今作ったArticleEntityの情報が反映されるように実装します。
ArticleListViewControllerにプライベートな変数としてArticleEntityの配列を持たせてやります。 それを、TableViewの行数に反映させて、セルの表示をEntityのタイトルから参照するようにします、
次は、記事詳細のほうです。
ArticleDetailViewControllerには、ArticleEntityのオブジェクトを1つだけ保持させるように変数を作ってやります。 あとで外から代入するので、スコープとしてはプライベートはつけません。 セルの表示ですけれども、Entityのタイトルと本文それぞれを適切にラベルに渡してやります。
起動時に記事一覧が出るようにする
先ほどは、とりあえずアプリ上の表示を見るために、設定ファイルで初期表示のストーリーボードを変更してアプリを実行してみましたが、 アプリを起動したら記事一覧画面が表示されるようにプログラムを書いていきたいと思います。
SceneDelegateの中身を追記していきます。 このあたりはVIPERとは関係のないところなので、サクッと書いていきます。
ストーリーボードからArticleListViewControllerを生成していきます。 ストーリーボードの名前を指定して、ルートViewコントローラを取得して、型のチェックもしておきます。 最後は、ナビゲーションコントローラを用意してあげて、その中に記事一覧画面を入れてWindowにセットします。 すると、このようにナビゲーションがついた一覧画面が表示されます。
import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) window?.makeKeyAndVisible() guard let articleListViewController = UIStoryboard(name: "ArticleList", bundle: nil).instantiateInitialViewController() as? ArticleListViewController else { fatalError() } let navigation = UINavigationController(rootViewController: articleListViewController) window?.rootViewController = navigation } // 省略 }
Interactorの作成
ここからは、VIPERの中の、Interactorを作っていきます。
ユースケース
Interactorは、ビジネスロジックに関する責務を負いますが、 単一責任の原則に基づいてクラスの数は1仕事につき1個というふうに設計をしていきます。 この1つの仕事を単位として「ユースケース」という名前で取り扱っていきたいと思います。
参考書籍
今回ですね、キュリオシティソフトウェアさんから刊行されています「VIPER研究読本」という書籍の中に ユースケースの実装の仕方にについて、すごく参考になる記載がありました。 その実装方法を参考にしてユースケースを組んでいこうと思います。
Interactor具象クラスの作成
実際にクラスを作っていきます。 今回は、2つのユースケースを作成していきます。
1つはGetArticlesArrayUseCase
です。
これは、WebAPIからデータを取得してきて、配列でArticleEntiityを返してくれるユースケースになる予定です。
それからもう1つは、WebAPIから記事のIDを指定して、1つだけ記事を取得し、ArticleEntiityを返してくれるユースケース。GetArticleByIdUseCase
です。
これらのユースケースの実装に入る前に、先ほども言ったとおり書籍を参考にして、これらのユースケースの共通部分を組み立てていこうと思います。
ユースケースの共通化
ここにUseCase.swift
というファイルを、用意しました。
まずは、ここにUseCaseProtocol
というプロトコルを定義します。
先ほど作ったユースケースのクラスたちは、このプロトコルに準拠させることになります。
では、その中身ですが、3つのアソシエートタイプを用意します。
Parameter
、Success
、Failure
です。
アソシエートタイプに付ける条件としては、FailureはError
に準拠していることとします。
そして、メソッドを1つexecute
というものを定義します。
この中では、まずParameterを第一引数に渡させて、第二引数に処理終了時のコールバックとしてのcompletion
を渡させるのですが、
そのコールバック内で取得できるものは、SuccessとFailureを入れたResultとしておきます。
ちょっとややこしいですけれども、このような形で定義しておきます。
import Foundation protocol UseCaseProtocol where Failure: Error { associatedtype Parameter associatedtype Success associatedtype Failure func execute(_ parameter: Parameter, completion: ((Result<Success, Failure>) -> ())?) }
続いてUseCase
というクラスを、定義していきます。
ここにはジェネリクスタイプとして、先ほどUseCaseProtocolのほうで定義した3つのアソシエートタイプと同じものを取らせるようにします。
続いて、イニシャライザを定義していきます。 ここではUseCaseProtocolに準拠したオブジェクトを元に初期化するように定義していきます。
where文の所に、このようにUseCaseProtocolのアソシエートタイプと合わせていく作業をしていきます。 ちょっと見やすいように、改行をしておきますね。
続いてですが、UseCaseProtocolに定義していたexecuteメソッドと同じシグネチャのメソッドを、UseCaseクラスに実装しておきます。 このメソッドの中身は後で書きますので、一旦スキップしておきます。
class UseCase<Parameter, Success, Failure: Error> { init<T: UseCaseProtocol>(_ useCase: T) where T.Parameter == Parameter, T.Success == Success, T.Failure == Failure { } func execute(_ parameter: Parameter, completion: ((Result<Success, Failure>) -> ())?) { } }
この次に、UseCaseのプライベートなextensionを追加して、サブクラスとしてUseCaseInctanceBase
というクラスを定義していきます。
ジェネリクスタイプとして、先ほどのUseCaseと同じものを指定してやります。
このクラスにも先ほどのUseCaseクラスの時と同じように、UseCaseProtocolと同じシグネチャのexecuteメソッドを作ってやります。
こちらは、この後の実装の時にお分かりになると思いますけれども、
このexecuteメソッドは通常通り使用すると、通ることのないデッドコードになりますので、この中身の実装はfatalErrorになることにします。
private extension UseCase { class UseCaseInstanceBase<Parameter, Success, Failure: Error> { func execute(_ parameter: Parameter, completion: ((Result<Success, Failure>) -> ())?) { fatalError() } } }
続いて、同じくサブクラスとしてUseCaseInstance
クラスを作ります。
これは、UseCaseInctanceBaseクラスの、継承クラスとなるのですが、UseCaseProtocolをT
としたジェネリクスタイプを取らせます。
さらに、親クラスでは3つのジェネリクスタイプを指定するのですが、これは全部UseCaseProtocolのものに合わせるようにします。
このクラスにはUseCaseProtocolに準拠したオブジェクトを持たせるようにして、
それを渡すためのイニシャライザも定義しておきます。
続いて、親クラスからオーバライドする形でexecuteメソッドを作ってやるのですが
ここで気をつけなければならないことは引数に持たせる型ですね。
これをT
のものにあわせてやらないとエラーになってしまいます。
そして、メソッドの中身は保持しているUseCaseProtocolに準拠したオブジェクトのexecuteメソッドを呼び出してやります。
private extension UseCase { class UseCaseInstanceBase<Parameter, Success, Failure: Error> { func execute(_ parameter: Parameter, completion: ((Result<Success, Failure>) -> ())?) { fatalError() } } class UseCaseInstance<T: UseCaseProtocol>: UseCaseInstanceBase<T.Parameter, T.Success, T.Failure> { private let useCase: T init(_ useCase: T) { self.useCase = useCase } override func execute(_ parameter: T.Parameter, completion: ((Result<T.Success, T.Failure>) -> ())?) { useCase.execute(parameter, completion: completion) } } }
先ほどスキップしていたUseCaseクラスの中身を実装をしていきます。 このように、インスタンスを保持するようにして、イニシャライザの中でジェネリクスを上手く使って渡してやります。 少し「保持する型」と「作成時の型」には注意してください。 executeメソッドは保持したインスタンスのexecuteメソッドを使用するようにします。
class UseCase<Parameter, Success, Failure: Error> { private let instance: UseCaseInstanceBase<Parameter, Success, Failure> init<T: UseCaseProtocol>(_ useCase: T) where T.Parameter == Parameter, T.Success == Success, T.Failure == Failure { self.instance = UseCaseInstance<T>(useCase) } func execute(_ parameter: Parameter, completion: ((Result<Success, Failure>) -> ())?) { instance.execute(parameter, completion: completion) } }
はい。ここまででユースケースに関する共通の部分ができあがりました。
ジェネリクスがいくつも絡むクラスやサブクラスが登場して、スッとは把握できなかったかもしれませんし、 どのような使い方になるのかイメージがつきにくかったかもわかりませんが、後からの説明で「あ、なるほど」と分かっていただけるかと思いますので、 現時点では「ここはこういうものだ」と思っていただく感じで大丈夫だと思います。 なんにせよ、これで簡潔かつ疎結合なユースケースが実現できるようになります。
UseCaseProtocolへの準拠と型解決
それでは、実際にUseCaseProtocolを各ユースケースのクラスに準拠させてみましょう。
このようにエラーが起きてしまいますが、executeメソッドのシグネチャを修正してやることでこれは解消されます。
実際に書いてみますが、このアソシエートタイプの型の部分さえ確定してあげればエラーは消えるというわけです。
この時の型ですが、記事をすべて取得するだけなので、その場合には特にパラメータは不必要です。
不必要な場合はVoid
と指定してやれば大丈夫です。
ResultのところのSuccessには、成功時に戻ってくるデータの型を指定してやります。 この場合は、ArticleEntityの配列を指定します。 Failureの箇所にはErrorを指定します。
今回は、WebAPIからの取得部分は後で書くことにしますので、今は直打ちの固定のデータが失敗することなく返ってくるようにします。 このように、3件のダミーの配列を作っておきます。 最後は、completionで呼び出し元に結果を渡してやるのですが、こうやってSuccessに配列を渡してやります。
import Foundation class GetArticlesArrayUseCase: UseCaseProtocol { func execute(_ parameter: Void, completion: ((Result<[ArticleEntity], Error>) -> ())?) { let res: [ArticleEntity] = [ ArticleEntity(userId: 1, id: 1, title: "タイトル", body: "本文"), ArticleEntity(userId: 1, id: 2, title: "タイトル2", body: "本文2"), ArticleEntity(userId: 1, id: 3, title: "タイトル3", body: "本文3"), ] completion?(.success(res)) } }
続いては、GetArticleByIdUseCaseを作っていきます。
先程と同じように、executeメソッドを定義してやることになるのですが、 今回はWebAPIに記事のIDを渡す必要がありますので、パラメータには記事IDの型であるIntを指定します。
そして、SuccessのところにはArticleEntityの配列ではなく、単体のArticleEntityを指定します。 Failureの箇所には、ErrorでOKです。
同じく、WebAPIからの取得部分は一旦飛ばすので、このように固定でダミーのArticleEntityオブジェクトを一つを作って、 completionで呼び出し元に結果を渡します。
これで、Interactor。すなわちユースケースの仮組みが終わりました。
import Foundation class GetArticleByIdUseCase: UseCaseProtocol { func execute(_ parameter: Int, completion: ((Result<ArticleEntity, Error>) -> ())?) { let res = ArticleEntity(userId: 1, id: 1, title: "タイトル", body: "本文") completion?(.success(res)) } }
Routerの作成
それでは次に、VIPERのRouterを作っていこうと思います。
今回画面遷移は、記事一覧画面から記事詳細画面へ遷移するという1個だけなので、Routerの数も1個だけにしようと思います。 Routerの数なんですけれども、今のように画面遷移が起きうる画面のPresenter」に対して1つのRouterというのが目安になるかなと思います。
まずは、Routerもプロトコルを定義するところから始めます。
名前はArticleListRouterProtocol
とします。
そして、具象型としてのクラスArticleListRouter
を定義して、ArticleListRouterProtocolに準拠させます。
次に、画面遷移を行うためにはビューコントローラの参照が必要になるので、それを変数に定義してイニシャライザで保持できるようにしておきます。 このとき、循環参照の問題を避けるために弱参照を明示しておく必要があります。
続いて、詳細画面に遷移するためのメソッドをプロトコルに定義します。
名前はshowArticleDetail
としておいて、どの記事の詳細を出すのかを引数で渡せるようにしておきます。
ArticleListRouterにこのメソッドを実装していきますが、実際の画面遷移はまだ作らずに、 どのIDの記事が選択されたのかをログ出力するだけに留めておきます。
Routerの仮組みは、以上になります。
import UIKit protocol ArticleListRouterProtocol: AnyObject { func showArticleDetail(articleEntity: ArticleEntity) } class ArticleListRouter: ArticleListRouterProtocol { weak var view: UIViewController! init(view: UIViewController) { self.view = view } func showArticleDetail(articleEntity: ArticleEntity) { print("詳細画面へ遷移する 記事ID: \(articleEntity.id)") } }
Presenterの作成
VIPERの中では最後の要素、Presenterを作っていこうと思います。
プロトコルと具象型の定義(記事一覧用)
まずは、記事一覧画面用のPresenterを作っていきますが、
Presenterもまた先程のRouterと同じように、プロトコルを定義するところから始めます。
名前はArticleListPresenterProtocol
とします。
さらにもうひとつ、Viewが準拠すべきプロトコルも定義していきます。
これをArticleListViewProtocol
とします。ここにView用のインターフェイスを用意する理由は、後ほどお話します。
では、Presenterクラスを作っていきましょう。
名前ArticleListPresenter
とします。
さきほどのRouterと同じように、弱参照のViewを1つ保持させるのですが、ここでは今作ったArticleListViewProtocolを指定して、
イニシャライザも作成していきます。
import Foundation protocol ArticleListPresenterProtocol: AnyObject { } protocol ArticleListViewProtocol: AnyObject { } class ArticleListPresenter { weak var view: ArticleListViewProtocol! init(view: ArticleListViewProtocol) { self.view = view } }
インターフェイスの定義
次に、ViewとPresenterの関係をインターフェイスベルで定義していきます。
今回は「画面が表示されると同時にデータを取りに行って、それを表示する」という動きにしたいので、
View側の「画面が表示された」というイベントを受理できるようにしたいです。
なので、Presenter側にはdidLoad
というメソッドをインターフェイスとして用意しておきます。
View側で起きうるイベントとしては、もう1つ。「一覧から記事を選択したとき」というイベントが考えられます。
これも、Presenter側で受理できるようにしておきたいので、ArticleEntityを引数にしたdidSelect
というメソッドをインターフェイスとして用意しておきます。
protocol ArticleListPresenterProtocol: AnyObject { func didLoad() func didSelect(articleEntity: ArticleEntity) }
続いて、View側です。 こちらは、処理を終えたPresenterが表示の指示をViewに出すためのメソッドのインターフェイスを用意していきます。
具体的には、取得してきた配列データを画面に表示してもらうためのメソッドshowArticles
を用意します。
次に、アプリによってはデータがなかった場合は「データがありませんでした」などの表示をすることもあるかと思います。
その場合分けは、View側ではなくPresenterが指示するようにします。
そのためのメソッドとしてshowEmpty
というメソッドを用意しておきます。
最後に、データの取得がネットワーク経由で行われることから、あらゆるエラーが想定されます。
エラーが発生した場合に、その表示をするのはViewの仕事なので、その窓口も作っておきます。
名前はerrorを引数にしたshowError
にしておきます。
protocol ArticleListViewProtocol: AnyObject { func showArticles(_ articleEntities: [ArticleEntity]) func showEmpty() func showError(_ error: Error) }
プロトコルへの準拠
では、実際にPresenterにこのPresenterプロトコルを準拠させていきましょう。 プロトコル用のextensionを新たに作ってやるのが、個人的には可読性が上がると思います。 このように、未実装エラーが出るので、FIXを押して実装すべきメソッドを補完してやります。少し整えますね。
次に、Viewコントローラ側にもViewプロトコルを準拠させてやります。 このようにViewコントローラにもプロトコル用のextensionを新たに作ってやります。 先ほどと同じように未実装エラーにはFIXを押して補完します。
extension ArticleListPresenter: ArticleListPresenterProtocol { func didLoad() { } func didSelect(articleEntity: ArticleEntity) { } }
Presenterの中身の実装
Presenterの中身を実際に実装していきましょう。
didLoad時に、先ほど作った配列取得用のユースケースGetArticlesArrayUseCase
を使ってデータを取得していきます。
先ほど、このユースケースのパラメータはVoid
と定義しましたので、executeの1つ目の引数は、このように空のカッコを渡してやります。
次にコールバックですが、Resultが渡されてきますので、このような形で記述します。
NullPointerエラーを避けるためにself
が強参照できるかどうかをguard文で判定します。
Resultの中身はSuccessかFailureなので、switch文を使って分岐と値の取得をこのように行います。
先に、失敗時の挙動を実装していきたいと思います。 ここでは、ViewのshowErrorを呼び出してViewに表示を依頼します。 Presenterはエラーを表示することは知っていますが、どのように表示するかは知らない状態になっているのがお分かりいただけるでしょうか。
次に、成功時の挙動も書いていきます。 成功時は記事の配列が返ってるので、それをViewのshowArticlesを呼び出して表示してやります。 ここでもPresenterは、どのように表示するかは知らない状態になっています。
さて、データが空だった場合はshowEmptyを呼び出すということも先ほど決めました。 なので、配列の結果によって処理が分岐するという「ロジック」をこのように実装しておきます。
extension ArticleListPresenter: ArticleListPresenterProtocol { func didLoad() { GetArticlesArrayUseCase().execute(()) { [weak self] result in guard let self = self else { return } switch result { case .success(let articleEntities): if articleEntities.isEmpty { self.view.showEmpty() return } self.view.showArticles(articleEntities) case .failure(let error): self.view.showError(error) } } } func didSelect(articleEntity: ArticleEntity) { } }
ビューコントローラ側の中身の実装
では、ビューコントローラ側も実装していきましょう。
まず、Presenterを保持するための変数を定義します。 型はPresenterクラスそのものではなく、Presenterプロトコルで指定してやります。
そして、ビューコントローラのviewDidLoad
イベント発生時にPresenterのdidLoadを呼び出すようにします。
class ArticleListViewController: UIViewController { var presenter: ArticleListPresenterProtocol! @IBOutlet private weak var tableView: UITableView! private var articleEntities = [ArticleEntity]() override func viewDidLoad() { super.viewDidLoad() presenter.didLoad() } }
Viewプロトコルの実装部分も作っていきましょう。 PresenterからshowArticlesの指示が来たときには、単純にオブジェクト変数に渡されてきた配列を保持してTableViewを再ロードします。 これで配列の中身が一覧に表示されるはずです。
次に、showEmptyが呼ばれたときですが、様々な表示方法が考えられますが、今回はshowArticlesと実装を同じにしておくことにします。
showErrorについても、エラーアラートを出すなどの表示方法が考えられますが、今回はスキップしておきます。
extension ArticleListViewController: ArticleListViewProtocol { func showArticles(_ articleEntities: [ArticleEntity]) { self.articleEntities = articleEntities tableView.reloadData() } func showEmpty() { tableView.isHidden = true showArticles([]) } func showError(_ error: Error) { // 今回はスキップ } }
ViewにPresenterの実体を渡す
ここまででPresenterとViewの実装ができました。 しかし、このまま実行するとアプリは落ちてしまいます。 なぜなら、Viewが保持すべきPresenterのインスタンスがどこにも生成されていないからです。
記事一覧画面が生成されるのは、先ほど作ったとおりSceneDelegateの中でした。 ここでPresenterを生成してやり、Viewに渡してやります。
Presenterを作る際には、View自体の参照を渡してやります。 Presenter側はViewを弱参照で保持するので、循環参照の問題は発生しません。
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) window?.makeKeyAndVisible() guard let articleListViewController = UIStoryboard(name: "ArticleList", bundle: nil).instantiateInitialViewController() as? ArticleListViewController else { fatalError() } articleListViewController.presenter = ArticleListPresenter(view: articleListViewController) let navigation = UINavigationController(rootViewController: articleListViewController) window?.rootViewController = navigation } // 省略 }
それでは、アプリを実行してみましょう。 先ほどユースケース内で実装した3件のデータがこのように表示されました。
依存性注入
まだ少しだけ問題が、残っています。この部分を見てください。
extension ArticleListPresenter: ArticleListPresenterProtocol { func didLoad() { GetArticlesArrayUseCase().execute(()) { [weak self] result in guard let self = self else { return } // 以下略
GetArticlesArrayUseCaseのインスタンスをPresenter自身が生成しています。 つまり、これはPresenterがInteractorのクラスを知ってしまっているがために起きる「密結合」という状態になってしまっています。
「密結合」はクラス単体のテストがしにくくなるなどのデメリットが大きな状態です。 これを解消するために、VIPERは「依存性注入」(Dependency Injection)によって、 外から使用するインスタンスを差し込んでやることで密結合の問題を解決させます。
Presenterの依存性定義(記事一覧)
では、実際にソースコードに、落とし込んでいきます。
まず、Presenterにサブ構造体を作成します。
Presenterが依存する要素を定義するためのものです。
名前はDependency
とします。
class ArticleListPresenter { struct Dependency { } weak var view: ArticleListViewProtocol! init(view: ArticleListViewProtocol) { self.view = view } }
記事一覧のPresenterが依存するものとは何でしょうか。
ひとつは、記事詳細へ画面遷移を担当するRouterです。 クラスでの指定ではなく、プロトコルで指定してやります。
もうひとつは、先ほども話したユースケースになります。 ここで、ユースケースの共通部分を作った際に「後で分かる」とお伝えした「強み」が発揮されることになります。
実体としては、GetArticlesArrayUseCaseを差し込みますが、そのGetArticlesArrayUseCaseの型を指定するのではなく、 UseCaseクラスを使ってこのようにParameterとResulttの成功と失敗の型のみで指定することができるのです。 これによりPresenterは、GetArticlesArrayUseCaseクラスに依存しなくなります。
struct Dependency { let router: ArticleListRouterProtocol! let getArticlesArrayUseCase: UseCase<Void, [ArticleEntity], Error> }
依存性注入の反映
では、Presenterに今の依存性注入部分を反映させます。 「di」というDependency型の変数を定義し、イニシャライザで渡すように変更します。 これで「Presenterを生成するときには依存性注入をしなければならない」という義務が生まれることになります。
class ArticleListPresenter { struct Dependency { let router: ArticleListRouterProtocol! let getArticlesArrayUseCase: UseCase<Void, [ArticleEntity], Error> } weak var view: ArticleListViewProtocol! private var di: Dependency init(view: ArticleListViewProtocol, inject dependency: Dependency) { self.view = view self.di = dependency } }
実際に依存性を注入する
この変更によって、先ほどSceneDelegateで実装したPresenter生成の際のパラメータが足りなくなります。 ここに依存性を注入していきましょう。
渡すViewはそのままです。 そして、第二引数にはDependencyをインスタンス化して渡しますが、 イニシャライザには必要なそれぞれの要素のインスタンスを作っていく必要があります。
まずはRouterですが、ここではビューコントローラの参照を渡して、ArtticleListRouterクラスのインスタンスを生成します。 そして、InteractorはUseCaseクラスを介してGetArticlesArrayUseCaseのインスタンスを渡すようにしてやります。 先ほどの3つのアソシエートタイプが合わない限りはエラーになるので、タイプセーフ(型安全)な状態になります。
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) window?.makeKeyAndVisible() guard let articleListViewController = UIStoryboard(name: "ArticleList", bundle: nil).instantiateInitialViewController() as? ArticleListViewController else { fatalError() } articleListViewController.presenter = ArticleListPresenter( view: articleListViewController, inject: ArticleListPresenter.Dependency( router: ArticleListRouter(view: articleListViewController), getArticlesArrayUseCase: UseCase(GetArticlesArrayUseCase())) ) let navigation = UINavigationController(rootViewController: articleListViewController) window?.rootViewController = navigation } // 以下略
密結合の解消
最後に、Presenter側で直接Interactorを生成していた箇所を、修正します。
diの中に注入された各要素が入っているので、ユースケースを呼び出す箇所はこのように修正します。
extension ArticleListPresenter: ArticleListPresenterProtocol { func didLoad() { di.getArticlesArrayUseCase.execute(()) { [weak self] result in
また、一覧から選択されたときは、Routerを使って画面遷移を実装します。
extension ArticleListPresenter: ArticleListPresenterProtocol { // 省略 func didSelect(articleEntity: ArticleEntity) { di.router.showArticleDetail(articleEntity: articleEntity) } }
こちらはView側ですが、PresenterのdidSelectを一覧から選択されたタイミングで呼び出されるようにこのように実装しておきましょう。
extension ArticleListViewController: UITableViewDelegate, UITableViewDataSource { // 省略 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) presenter.didSelect(articleEntity: articleEntities[indexPath.row]) } }
では、アプリを実行してみます。 先ほどと同じようにユースケースからデータが取得され、Routerが反応していることが分かると思います。
記事詳細のPresenterの作成
同様に記事詳細のPresenterを作っていきます。 一覧と同じような作り方なので、どんどんと進めていきます。
ViewとPresenterのインターフェイスを決めてやり、 Dependencyを作って依存関係を指定してやります。 今回はRouterは作らず、ユースケースだけです。 ユースケースのアソシエートタイプをこのように指定していきます。 ViewとDIによるイニシャライザをこのように作っていきます。
Presenterの実装についても、一覧と近いものになります。
記事一覧から処理をコピーしてきて、 ユースケースのexecuteの引数に記事のIDを渡すようにしてやり、 showEmptyは詳細には必要ないのでそのロジックを削ってやり、 Viewで呼び出すメソッドもこのように変更してやるだけです。
import Foundation protocol ArticleDetailPresenterProtocol: AnyObject { func didLoad(articleEntity: ArticleEntity) } protocol ArticleDetailViewProtocol: AnyObject { func showArticle(_ articleEntity: ArticleEntity) func showError(_ error: Error) } class ArticleDetailPresenter { struct Dependency { let getArticleByIdUseCase: UseCase<Int, ArticleEntity, Error> } weak var view: ArticleDetailViewProtocol! private var di: Dependency init(view: ArticleDetailViewProtocol, inject dependency: Dependency) { self.view = view self.di = dependency } } extension ArticleDetailPresenter: ArticleDetailPresenterProtocol { func didLoad(articleEntity: ArticleEntity) { di.getArticleByIdUseCase.execute(articleEntity.id) { [weak self] result in guard let self = self else { return } switch result { case .success(let articleEntity): self.view.showArticle(articleEntity) case .failure(let error): self.view.showError(error) } } } }
View側もほとんど一覧と変わりません。
Presenter用の変数をプロトコルで取れるようにして、 viewDidLoadでPresenterのdidLoadを呼ぶようにして、 ViewにViewプロトコルを準拠させ、未実装エラーにはFIXをし、 showErrorの内容とshowArticleの内容を実装していきます。
import UIKit class ArticleDetailViewController: UIViewController { enum Row: String { case title case body static var rows: [Row] { return [.title, .body] } } var articleEntity: ArticleEntity! var presenter: ArticleDetailPresenterProtocol! @IBOutlet private weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() presenter.didLoad(articleEntity: articleEntity) } } // 省略 extension ArticleDetailViewController: ArticleDetailViewProtocol { func showArticle(_ articleEntity: ArticleEntity) { self.articleEntity = articleEntity tableView.reloadData() } func showError(_ error: Error) { // 今回はスキップ } }
Routerの本実装
仮組だったRouterの、実装をしていきます。 ログ出力はもう要らないので削除します。
この中で記事詳細画面を生成していきます。 SceneDelegateで記事一覧を生成した内容とほぼ同じになると思います。 このメソッドには選ばれたArticleEntityが渡されてくるので、それをViewに渡します。
そして、Presenterの生成をしますが、先ほど同様にこの時点で依存性の注入を行っていきます。 今回の場合は、GetArticleByIdUseCaseになるので、ユースケースを介してそのインスタンスを渡してやります。
最後に、ナビゲーションコントローラに、今作った詳細画面を、プッシュしてやります。
import UIKit protocol ArticleListRouterProtocol: AnyObject { func showArticleDetail(articleEntity: ArticleEntity) } class ArticleListRouter: ArticleListRouterProtocol { weak var view: UIViewController! init(view: UIViewController) { self.view = view } func showArticleDetail(articleEntity: ArticleEntity) { guard let articleDetailViewController = UIStoryboard(name: "ArticleDetail", bundle: nil).instantiateInitialViewController() as? ArticleDetailViewController else { fatalError() } articleDetailViewController.articleEntity = articleEntity articleDetailViewController.presenter = ArticleDetailPresenter( view: articleDetailViewController, inject: ArticleDetailPresenter.Dependency( getArticleByIdUseCase: UseCase(GetArticleByIdUseCase()) ) ) view.navigationController?.pushViewController(articleDetailViewController, animated: true) } }
できあがったら、アプリを実行します。 一覧画面から選択すると、詳細画面に画面遷移をすることが確認できました。
Interactorの本実装
ここから仕上げになります。 ネットワーク経由でJSONを取得し、それをアプリに表示させるようにしたいと思います。
EntityのCodable準拠
そのための準備としてまず、ArtiicleEntityをCodableに準拠させるようにします。 JSON文字列から簡単にEntityオブジェクトを生成できるようにするためです。
struct ArticleEntity: Codable { let id: Int let userId: Int let title: String let body: String }
GetArticlesArrayUseCaseの本実装
仮組みだったユースケースの中身も一旦削除します。
ネットワークを使う処理ですが、今回は特にネットワーク系のライブラリなどは使わずに、
標準で搭載されているURLSession
を使って取得をしにいくようにします。
今回はサンプルなのでURLは直に書くようにしますが、 実際の開発では、どこかに定数で持たせるなどの工夫をするようにしてください。
dataTaskのコールバックに非同期処理後の処理を書いていきます。 エラーが発生した場合は、completionにエラーを添えたFailureを渡してやって処理抜けをします。 このとき、この処理はメインスレッド以外の場所で行われますので、completionの呼び出しはメインスレッドで行うようにしておくと良いと思います。
次に成功時ですが、取得されたJsonデータをJSONDecoder
によってデコードしてやります。
型はArticleEntityの配列にします。
ここではパースエラーなどが起きる可能性があるので、guard文のelseに入ってしまった場合は、エラー扱いにして処理抜けさせておきます。
最後に、メインスレッドでデコードに成功したデータをcompletionで渡してやります。 taskをレジュームさせて実装完了です。
class GetArticlesArrayUseCase: UseCaseProtocol { func execute(_ parameter: Void, completion: ((Result<[ArticleEntity], Error>) -> ())?) { let session = URLSession(configuration: .default) let url = URL(string: "https://jsonplaceholder.typicode.com/posts")! let task = session.dataTask(with: url) { data, response, error in if let error = error { DispatchQueue.main.async { completion?(.failure(error)) } return } guard let data = data, let decoded = try? JSONDecoder().decode([ArticleEntity].self, from: data) else { let error = NSError( domain: "parse-error", code: 1, userInfo: nil ) DispatchQueue.main.async { completion?(.failure(error)) } return } DispatchQueue.main.async { completion?(.success(decoded)) } } task.resume() } }
これで、WebAPI経由でデータを取得できるようになったはずなので、アプリを実行してみましょう。 このようにデータが取得されました。
ユースケースの実装の変更だけで、このようにデータの内容を、変更することができました。 しかし、詳細画面の方は固定のデータのままです。 こちらも変えていきましょう。
GetArticleByIdUseCaseの本実装
GetArticlesArrayUseCaseの内容をほぼ流用できるので、一旦コピーします。 変更するのはURLに記事のIDを指定してやることと、デコードする型を変えてやることです。
class GetArticleByIdUseCase: UseCaseProtocol { func execute(_ parameter: Int, completion: ((Result<ArticleEntity, Error>) -> ())?) { let session = URLSession(configuration: .default) let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(parameter)")! let task = session.dataTask(with: url) { data, response, error in if let error = error { DispatchQueue.main.async { completion?(.failure(error)) } return } guard let data = data, let decoded = try? JSONDecoder().decode(ArticleEntity.self, from: data) else { let error = NSError( domain: "parse-error", code: 1, userInfo: nil ) DispatchQueue.main.async { completion?(.failure(error)) } return } DispatchQueue.main.async { completion?(.success(decoded)) } } task.resume() } }
では、アプリを実行してやります。 一覧から記事を選択してやると、選択した記事の詳細が表示されるようになりました。 別の記事を選択すると、その記事の詳細が選択されています。
これで、このアプリは完成になります!!
ソースコードを書きながら、アプリをひとつ作ってみました。 長丁場になりましたが、いかがでしたでしょうか。
VIPERの深堀り
ここからは、もうすこしVIPERの各要素について、お話をしようと思います。
View
まず、Viewについてなのですが、完成したソースコードにはデータの取得についての記述がないことがお分かりいただけると思います。
今回は、ユーザのアクションとしては「一覧から選択する」というものしかありませんが、 もちろん「ボタンが押された」「スイッチを切り替えた」「フォームに文字を入力した」「スワイプした」など、いろんなイベントが考えられます。 これらのイベントは、Presenterに丸投げすることによりViewは見た目についてのみ注力できます。
今回の例で言えば、実装はスキップしましたが、エンプティ表示をさせるためにTableViewは再ロードするのではなく、 非可視にするなど「どう表現するか」のみを実装しておけば、データの中身がどんな風になっているかは考えず「エンプティ表示をどう見せるか」だけを考えれば良いわけです。
Viewのソースコードは、Viewロジックについての責務は負わないというふうにお話をしてきました。 つまりは、Viewのソースコードにはif文やfor文などは極力無くなっていくはずです。
しかし、たとえばデータの中に何かしらのステータス情報があり、 それを表示させるときにはラベルの背景色や文字の太さが変わるといった表示仕様であるとどうでしょうか。
見た目の分岐であるわけですからPresenterが担当すべきようにも見えますが、 色とフォントを抽象化したUIColorやUIFontはUIKitを使用する部分です。 Presenterは、原則としてUIKitはインポートしないほうが好ましいです。 ですので、この場合の表示分岐はViewが担当する方がいいと思いますが、 だからといって、それらをViewにゴリゴリと書いてしまうと、それもまた冗長な実装になります。 ステータスに応じて背景色やフォントを返すextensiionなどを作り、見通しを良くする工夫をしてみてください。
Presenter
次は、Presenterのお話です。
VIPERの相関図ではPresenterが中央に位置しているので、もしかするとPresenterに色々な実装を書いてしまうかもしれません。 そうしてしまうとビューコントローラのファットな部分が、単純にPresenterに移っただけの「Fat Presenter」を生み出してしまいそうです。 なので、Presenterを実装する際には、ひとつの原則を意識するようにしたほうがいいかなと思います。
先ほどの記事一覧のPresenterを見てみます。 クラスの元々の定義はViewの参照と依存性、そしてそのイニシャライザのみです。
そして、Presenterプロトコルの実装部分を見ていきます。 didLoadが呼ばれたとき、つまり「画面が初期表示しようとするとき」に「データ取得を実行」し、 「成功した場合、数が0件ならばエンプティ表示、それ以外ならデータを表示させる」 「失敗した場合は、エラーを表示する」という記述になっていると思います。 didSelectについても「選ばれたときには、記事詳細を表示する」としています。
これは、なんとなくあるものに、イメージが近くないでしょうか。
そう「アプリの仕様書」です。
Presenterには、原則として「アプリでこうされたときには、こうなる」というものだけを落とし込まれた状態にすれば、 Presenterの実装自体が仕様書のように見えてくるはずです。
ViewやPresenterのインターフェイスの命名も、そのあたりを意識すると更に分かりやすくなるはずです。
この原則を貫くことにより、アプリが大きくなってきてPresenterのコード量があがってきてしまっても、見通しの悪いものにはならないはずです。 画面の要素が多くなり、可読性が下がってきたなと思ったら、1つのViewに対して複数のPresenterを持たせる戦略もアリですが、 まずはこの原則に基づいているかを見直すようにしてみてください。
実は、Presenter側にView用のインターフェイスを用意した理由はここにあります。 設計方針として、プロトコルはプロトコルでファイルを用意して定義をするという方法も間違いではないと思います。 要するに、抽象と具象の完全分離ですね。
ただし、今お話した通り、Presenter自身が仕様書に近いものになるとすれば、その変更や追加がひとつのファイルで定義できると楽になるかなと思います。
どんなイベントがPresenterに吸収され、どのようにViewに指示されるかの見通しを良くするため、 自分は、Presenterが定義されたファイルにViewとPresenterのプロトコルも書くようにしています。
Interactor
Interactorについては、今回は「単一責任の原則」にもとづいて「1仕事、1クラス」という作り方にしました。
今回のサンプルアプリで、例えば「お気に入りの記事の登録する」とか「取得したデータはキャッシュファイルに保存する」などの機能追加をするかもしれません。 しかし、それを「WebAPIからデータを取得するクラスに、メソッドを追加しよう」としたりはしません。
これらの仕事に対しては、やはり1クラスずつ作って、その作業に集中させてやることが必要かと思います。 そうすることの理由としては機能の「取り外し」と「差し替え」が容易になるからです。
先程のコーディングでも見ていただいたとおり、仮のデータを返すユースケースから、 実際にAPIを叩いてデータを返すユースケースが簡単にすげ替えることができたのを見ていただいたと思います。
依存性注入によってユースケースを仮実装のものから本実装のものに変えていけますし、 あるいはテスト用のスタブに変えてテストさせることも容易にできるようになるのです。 ただし、この方法はファイル数がどんどん膨れ上がっていきます。 実装の手間も大きいので、アプリの規模と残りスケジュールなどを相談する必要もあると思います。
Router
次にRouterですが、純粋なVIPERの考えに基づいて実戦投入してみると色々と気づくことがあり、少し原則から外す方法を取ることにしました。
今回のサンプルでは、ひとつのメソッドの中でストーリーボードからインスタンスを作り、 依存性注入を行ってナビゲーションコントローラを使って画面遷移を行わせました。
しかし、実践的な開発の際には、この「インスタンス生成」と「画面遷移」は別々に定義する方法を取りました。 これは、同じViewコントローラだけども、初期のパラメータを変えることによって別の画面として振る舞わせるというケースのときに役立ちました。
例えば、全面にWebViewが置かれているViewコントローラを生成するメソッドを定義しておき、 アプリではよくある「利用規約」や「プライバシーポリシー」などのコンテンツを出し分けしたいときに、 画面遷移だけに注力させるメソッドを定義し、渡すURLを分岐させていきます。
Routerを呼び出すPresenter側は、Routerの決まったメソッドを呼び出すだけでいいのでシンプルですし、共通化した分だけソースコードも減ります。
こうなってくると、1画面につき1Routerという原則だと窮屈になります。 そこで原則は破る形で複数の画面をグループ化し、グループごとに1つのRouterを作成するようにし、 PresenterにはRouterを使用するグループの数だけ取らせるようにしました。
このあたりも実現方法はいくつかあると思うので、色々と試してみると面白いかもしれません。
Entity
最後に、Entityについてです。 Entityも実戦投入してみると気付きを感じました。
今回は整数型と文字列型だけのシンプルな構造体でしたが、APIから日付や色情報、緯度経度など様々な情報が返ってくることも想定されます。 しかし、Codableに準拠させたEntity構造体は、プリミティブな値以外を持たせようとすると複雑になってしまいます。
そこで、Entity構造体はそのままに、アプリ内で使用するために抽象化したモデルクラスを別途定義することにしました。 これにより、APIから返されるレスポンスの定義もソースコードから見やすくなり、抽象化されたモデルオブジェクトがEntityとして振る舞うことにより、柔軟な組み方ができるようになりました。
Entity構造体からモデルオブジェクトに変換するための中間クラスも定義しておけば、実装者によって抽象化の方法が変わるという問題も減るかと思います。
まとめ
ここからは、まとめになります。
VIPERのまとめ
まず、VIPER自体についてです。
VIPERは「iOSアプリ開発に特化したシステムアーキテクチャのひとつ」ということでした。
要素同士はプロトコル(インターフェイス)同士で繋がり合わせることで、密結合を避けるようにします。
依存性注入を使うことで、その密な依存を解消することができます。
単一責任の原則によって、ややこしい仕様も、可読性をできるだけ落とさない工夫ができるようになります。
Viewが見た目に、Routerが画面遷移に集中し、Presenterは仕様書のように振る舞い、Interactorの1クラスが1仕事に注力することで、 太っていってしまうスパゲッティコードを解消していくことができます。
VIPERのデメリット
逆にデメリットも上げていこうと思います。
先ほども軽く触れましたが、VIPERはファイル数が非常に多くなります。 見ていただいた通り、2つの簡単な画面だけでも相当ファイル数があったと思います。 VIPER用のファイルジェネレータもネットで拾うことができますが、つまりは、そういった苦労があるので、そうしたツールが作られているとも言えます。
また、VIPERは学習コストが多少かかる印象です。 個人で作るときはもちろん、複数人で開発をする際には、まずVIPERの目的や組み方を把握しておく必要がありそうです。 チーム開発で始める場合は、今回のような小さなサンプルを組んでみて、各要素の役割と責務の範囲の認識を合わせておいたほうがいいのではないかと思います。
最後に
最初にお話したとおり、VIPERは銀の弾丸ではありません。 自分の今回発表したサンプルコードや説明も、まだまだアップデートをする余地は大いにあると思います。
VIPERのサンプルはネット上にもたくさんあり、フォルダ構成しかり、命名規則しかり、組み方しかり、それぞれ少しずつ違いがあります。 まずはひとつ、そのとおりに試してみて、VIPERの目的を外さないように自分の組みやすい方法を模索していく、そんな「守破離」な方法も手かなと思っています。
長くなりましたが、今回の発表がシステムアーキテクチャに悩むどなたかの何かの役に立てれば幸いです。
ご視聴ありがとうございました。またよろしくおねがいします。
さよなら